Skip to content

Security: fix open redirect + timing-safe HMAC verify#2

Open
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
devin/1776568749-security-fixes
Open

Security: fix open redirect + timing-safe HMAC verify#2
devin-ai-integration[bot] wants to merge 2 commits intomainfrom
devin/1776568749-security-fixes

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 19, 2026

Summary

Addresses the two Devin Review flags from #1:

  1. Open redirect in /login?next=… — the next form/query value was passed directly into redirect(), so /login?next=https://evil.com would bounce an authenticated user off-site. Now routed through a safeNext() helper that forces next to be a same-origin relative path (/…) and strips protocol-relative (//evil.com) and backslash-prefixed tricks. The hidden form input also uses the sanitized value so it can't be replayed.

  2. Timing-unsafe HMAC comparison in lib/session.tsexpected === signed was replaced with crypto.subtle.verify("HMAC", key, sig, data), which does constant-time comparison under the hood. Added a small fromHex helper to decode the signature bytes. Still fully Edge-runtime safe (no node:crypto).

Build + lint pass locally.

Review & Testing Checklist for Human

  • Hit https://kilo-control-8955.d.kiloapps.io/login?next=https://example.com → sign in → should land on /, not example.com.
  • Hit /login?next=//evil.com → should also collapse to /.
  • Hit /login?next=/profiles → should still land on /profiles after login (normal case still works).
  • Tamper with the kilo_control_session cookie value (flip a hex char in the signature) → reload → should bounce back to /login (verify still rejects invalid sigs).
  • Valid session cookie still authenticates (don't get logged out after redeploy).

Notes

Same-branch deploy target is https://kilo-control-8955.d.kiloapps.io. A fresh Kilo Deploy build should pick up main once this merges; the existing running container keeps serving in the meantime.

Link to Devin session: https://app.devin.ai/sessions/a0624ee5b9a847728a5ed20d9d0481ab
Requested by: @itsablabla


Open in Devin Review

@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Comment thread lib/session.ts Outdated
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
const byte = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
if (Number.isNaN(byte)) return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: parseInt silently ignores non-hex trailing characters, so Number.isNaN alone does not fully validate the input.

In JavaScript, parseInt('0g', 16) returns 0 (not NaN), and parseInt('ag', 16) returns 10. This means a hex pair like '0g' passes the isNaN check and is silently coerced to 0x00 instead of being rejected. While this is unlikely to be exploitable in practice (the HMAC comparison would simply fail with an incorrect byte), it is a correctness bug — fromHex may accept malformed hex strings without returning null.

A safer check is to validate the full hex string with a regex before parsing:

if (!/^[0-9a-fA-F]+$/.test(hex)) return null;

Or validate each pair with /^[0-9a-fA-F]{2}$/ before calling parseInt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in aa8bd44 — added a full-string regex validation (/^[0-9a-fA-F]+$/) plus an empty-string reject before the parseInt loop, so malformed pairs like "0g" return null rather than coercing to 0x00.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented Apr 19, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Previously Flagged — Now Resolved

File Issue Status
lib/session.ts parseInt silently accepted non-hex trailing chars ('0g'0), making the isNaN guard incomplete ✅ Fixed in aa8bd44 — full-string regex /^[0-9a-fA-F]+$/ + empty-string rejection added before the loop
Files Reviewed (2 files)
  • app/login/page.tsx — no issues
  • lib/session.ts — no issues (previous WARNING resolved)

Reviewed by claude-4.6-sonnet-20260217 · 178,251 tokens

devin-ai-integration[bot]

This comment was marked as resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant